package pathfinding;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import ownserver.ServerShip;

import base.Timestamped;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.DataManager;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedReference;

public class DStarPathfinderSimple implements IPathfinder <GridCell>, Serializable, ManagedObject {

	private ManagedReference<DStarGridDiagSimpleTabled> finderRef = null;
	ManagedReference<ServerShip> shipRef;
	ManagedReference<SpaceTimeReservationTable> tableRef;
	GridCell goal = null;
	GridCell start = null;
	AStarTabled comparison;
	
	// store the plan and info if the plan is optimal only because this way we know when to update the A* search
	List <GridCell> currentPlan = Collections.emptyList();
	boolean isPlanOptimal = false;
	boolean replanAStar = true;
	private int searchIndex = 0;
	
	private static final long serialVersionUID = 1L;
	
	/** The {@link Logger} for this class. */
    private static final Logger logger =
        Logger.getLogger(DStarPathfinderSimple.class.getName());
	
	public DStarPathfinderSimple(ManagedReference<SpaceTimeReservationTable> tableRef, ManagedReference<ServerShip> shipRef)
	{
		this.tableRef = tableRef;
		this.shipRef = shipRef;
		AppContext.getDataManager().markForUpdate(this);
		comparison = new AStarTabled(tableRef);
		
		tableRef.get().addWatcher(AppContext.getDataManager().createReference(this));
		
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM_dd__HH_mm_ss");
		Date myDate = new Date();
		filename = "pathfindingstats_" + sdf.format(myDate) + ".csv";
		
		writeCSVHeader();
	}
	private String filename;
	
	
	private void recreateFinder(GridCell start, GridCell goal)
	{
		DataManager dataManager = AppContext.getDataManager();
		if (finderRef != null) {
			//tableRef.get().removeWatcher(finderRef);
			dataManager.removeObject(finderRef.get());
		}
		DStarGridDiagSimpleTabled finder = new DStarGridDiagSimpleTabled(start, goal, tableRef);
		this.start = start;
		this.goal = goal;
		finderRef = dataManager.createReference(finder);
		
		//tableRef.get().addWatcher(finderRef);
		dataManager.markForUpdate(finder);
		dataManager.markForUpdate(this);
	}

	@Override
	public ArrayList<Timestamped<GridCell>> getPath(GridCell start,
			GridCell goal) {
		DataManager dataManager = AppContext.getDataManager();
		DStarGridDiagSimpleTabled finder;
		//dataManager.markForUpdate(this);
		if (finderRef == null || goal.equals(this.goal) == false)
		{
			recreateFinder(start, goal);
			finder = finderRef.get();
		}
		else { 
			finder = finderRef.get();
			if (start.equals(this.start) == false) {
				this.start = start;
				finder.updateStart(start);
				dataManager.markForUpdate(this);
				//dataManager.markForUpdate(finder);
			}
		}
		
		removeReservations();
		finder.resetStatisticsCounters();
		long begin = System.currentTimeMillis();
		currentPlan = finder.plan();
		long end = System.currentTimeMillis();
		
		dataManager.markForUpdate(finder); // planning usually changes the inner state of the finder
		AppContext.getDataManager().markForUpdate(this);
		
		int shortestRoute = GridGeometry.amountOfSquaresBetween(start, goal) + 1;
		
		comparison.resetStatisticsCounters();
		if (replanAStar) {
			comparison.search(start, goal);
			replanAStar = false;
		}
		
		if (shortestRoute == currentPlan.size()) {
			isPlanOptimal = true;
		}
		else
		{
			isPlanOptimal = false;
		}
		
		// plus one because planned routes contain the beginning and the end grids
		printStatistics(end - begin, shortestRoute, currentPlan.size(), finder);
		finder.resetModifiedStatesCounter();
		/*
		String s = "Plan from " + start + " to " + goal + ":";
		
		for (GridCell c : plan) {
			s += c + " ";
		}
		logger.log(Level.INFO, s);
		*/
		
		ArrayList<Timestamped<GridCell>> stampedPlan = new ArrayList<Timestamped<GridCell>>(currentPlan.size());
		
		long timeStamp = System.currentTimeMillis();
		long gridChangeTimeStraight = (long) (Math.ceil(GridGeometry.gridSize / shipRef.get().getMaxVelocity() * 1000.0));
		long gridChangeTimeDiag = (long) (Math.ceil(GridGeometry.gridSize / shipRef.get().getMaxVelocity() * 1000.0 * Math.sqrt(2.0)));
		
		GridCell earlier = null;
		for (GridCell c : currentPlan) {
			if (earlier != null) {
				if (GridGeometry.isDiagonalNeighbour(earlier, c)) {
					timeStamp += gridChangeTimeDiag;
				}
				else
				{
					timeStamp += gridChangeTimeStraight;
				}
			}
			
			Timestamped<GridCell> t = new Timestamped<GridCell>(c, timeStamp);
			stampedPlan.add(t);
			
			earlier = c;
		}
		
		addReservations(stampedPlan);
		
		searchIndex++;
		return stampedPlan;
	}
	
	public void printStatistics(long dt, int shortestRouteLength, int routeLength, DStarGridDiagSimpleTabled finder)
	{
		writeToFile(getStatisticsCSV(shortestRouteLength, routeLength, finder));
		logger.log(Level.INFO, getStatistics(dt, shortestRouteLength, routeLength, finder));
	}
	
	private void writeToFile(String output)
	{
		FileOutputStream fos;
		OutputStreamWriter outStream;
		// file is opened on every run because can't serialize outputstreams
		try {
			fos = new FileOutputStream(filename, true);
			outStream = new OutputStreamWriter(fos/*, "UTF-8"*/);
			try {
				outStream.write(output + "\n");
				outStream.close();
			}
			catch (IOException e) {
			}
			
		} catch (FileNotFoundException e) {
		}
	}
	
	private void writeCSVHeader()
	{
		writeToFile("Search index,Straight path,Found route,Max nodes stored during search,Nodes stored after search,Nodes modified after last search,Stored node changes,D* Lite read/write nodes,D* Lite expanded nodes,A* read/write nodes,A* expanded nodes");
	}
	
	private String getStatisticsCSV(int shortestRouteLength, int routeLength, DStarGridDiagSimpleTabled finder)
	{
		String result = new String();
		result += searchIndex + ",";
		result += shortestRouteLength + ",";
		result += routeLength + ",";
		result += finder.getMaxAmountOfStatesStored() +",";
		result += finder.getAmountOfStatesStored() + ",";
		result += finder.getTotalStatesUpdated() + ",";
		result += finder.getActualChangedStates() + ",";
		result += finder.getTravelledStates() + ",";
		result += finder.getExpandedStates() + ",";
		result += comparison.getTravelledStates() +",";
		result += comparison.getExpandedStates();
		return result;
	}
	
	public String getStatistics(long dt, int shortestRouteLength, int routeLength, DStarGridDiagSimpleTabled finder)
	{
		String result = new String();
		result += "Straight path: " + shortestRouteLength + ",";
		result += " found route: " + routeLength + ".";
		result += " Max nodes during search: " + finder.getMaxAmountOfStatesStored();
		result += ". Nodes after search: " + finder.getAmountOfStatesStored() + ".";
		result += " Total nodes modified after last search: " + finder.getTotalStatesUpdated() + ".";
		result += " Relevant nodes modified: " + finder.getActualChangedStates() + ".";
		result += " Read/write " + finder.getTravelledStates() + " nodes,";
		result += " expanded " + finder.getExpandedStates() + " nodes.";
		//result += " Percolations: " + finder.getPercolations() + ".";
		//result += " Search ran for " + dt + " milliseconds.";
		//result += " " + finder.getInnerDatastructures();
		result += " A* Search read/write " + comparison.getTravelledStates();
		result += " nodes, expanded " + comparison.getExpandedStates() + " nodes ";
		return result;
	}
	/*
	public String getInnerDatastructures()
	{
		return finderRef.get().getInnerDatastructures();
	}
	*/
	
	private void removeReservations()
	{
		tableRef.get().removeReservations(shipRef, false);
	}
	
	private void addReservations(ArrayList<Timestamped<GridCell>> stampedPlan) {
		// TODO!
	}
	
	@Override
	public double getCost(ArrayList<Timestamped<GridCell>> path) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public boolean needsUpdate() {
		if (finderRef == null) {
			return false;
		}
		return finderRef.get().needsUpdate();
	}

	public void changedToReserved(GridCell c)
	{
		if (finderRef == null) {
			return;
		}
		finderRef.get().changedToReserved(c);
		
		// check if we need to replan A* search
		if (replanAStar == false && currentPlan.contains(c)) {
			replanAStar = true;
			AppContext.getDataManager().markForUpdate(this);
		}
	}
	
	public void changedToFree(GridCell c)
	{
		if (finderRef == null) {
			return;
		}
		finderRef.get().changedToFree(c);
		
		// check if we need to replan A* search
		if (replanAStar == false && isPlanOptimal == false) {
			replanAStar = true;
			AppContext.getDataManager().markForUpdate(this);
		}
	}
}
